D:\a\csshw\csshw\src\daemon\mod.rs
Line | Count | Source |
1 | | //! Daemon imlementation |
2 | | |
3 | | #![deny(clippy::implicit_return)] |
4 | | #![allow(clippy::needless_return, clippy::doc_overindented_list_items)] |
5 | | #![warn(missing_docs)] |
6 | | |
7 | | use std::cmp::max; |
8 | | use std::collections::HashMap; |
9 | | use std::{ |
10 | | io, |
11 | | sync::{Arc, Mutex}, |
12 | | time::Duration, |
13 | | }; |
14 | | use std::{thread, time}; |
15 | | |
16 | | use crate::get_console_window_handle; |
17 | | use crate::utils::config::{Cluster, DaemonConfig}; |
18 | | use crate::utils::debug::StringRepr; |
19 | | use crate::utils::windows::{clear_screen, set_console_color, WindowsApi}; |
20 | | use crate::{ |
21 | | serde::{ |
22 | | deserialization::deserialize_pid, serialization::serialize_input_record_0, |
23 | | SERIALIZED_INPUT_RECORD_0_LENGTH, SERIALIZED_PID_LENGTH, |
24 | | }, |
25 | | spawn_console_process, |
26 | | utils::{ |
27 | | constants::{PIPE_NAME, PKG_NAME}, |
28 | | windows::{ |
29 | | arrange_console, get_console_input_buffer, read_keyboard_input, |
30 | | set_console_border_color, |
31 | | }, |
32 | | }, |
33 | | WindowsSettingsDefaultTerminalApplicationGuard, |
34 | | }; |
35 | | use bracoxide::explode; |
36 | | use log::{debug, error, warn}; |
37 | | use tokio::sync::broadcast::error::TryRecvError; |
38 | | use tokio::{ |
39 | | net::windows::named_pipe::{NamedPipeServer, PipeMode, ServerOptions}, |
40 | | sync::broadcast::{self, Receiver, Sender}, |
41 | | task::JoinHandle, |
42 | | }; |
43 | | use windows::Win32::System::Console::{ |
44 | | CONSOLE_CHARACTER_ATTRIBUTES, INPUT_RECORD_0, LEFT_CTRL_PRESSED, RIGHT_CTRL_PRESSED, |
45 | | }; |
46 | | |
47 | | use windows::Win32::UI::Input::KeyboardAndMouse::{ |
48 | | VIRTUAL_KEY, VK_A, VK_C, VK_E, VK_ESCAPE, VK_H, VK_R, VK_T, |
49 | | }; |
50 | | use windows::Win32::UI::WindowsAndMessaging::{SW_RESTORE, SW_SHOWMINIMIZED}; |
51 | | use windows::Win32::{ |
52 | | Foundation::{COLORREF, HANDLE, HWND, STILL_ACTIVE}, |
53 | | System::{Console::ENABLE_PROCESSED_INPUT, Threading::PROCESS_QUERY_INFORMATION}, |
54 | | }; |
55 | | |
56 | | use self::workspace::WorkspaceArea; |
57 | | |
58 | | mod workspace; |
59 | | |
60 | | /// The capacity of the broadcast channel used |
61 | | /// to send the input records read from the console input buffer |
62 | | /// to the named pipe servers connected to each client in parallel. |
63 | | const SENDER_CAPACITY: usize = 1024 * 1024; |
64 | | |
65 | | /// Runtime state of a client's assigned pipe server task. |
66 | | /// |
67 | | /// Observed by the pipe server on each input record; determines whether |
68 | | /// the record is forwarded to the client over the named pipe. |
69 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
70 | | enum PipeServerState { |
71 | | /// Forward all input records to the client. |
72 | | Enabled, |
73 | | } |
74 | | |
75 | | /// Representation of a client |
76 | | #[derive(Clone)] |
77 | | struct Client { |
78 | | /// Hostname the client is connect to (or supposed to connect to). |
79 | | hostname: String, |
80 | | /// Window handle to the clients console window. |
81 | | window_handle: HWND, |
82 | | /// Process handle to the client process. |
83 | | process_handle: HANDLE, |
84 | | /// Process id of the client process. |
85 | | /// |
86 | | /// Used by the pipe server task to correlate which client has connected |
87 | | /// to it, via a handshake over the named pipe. |
88 | | process_id: u32, |
89 | | /// Shared state between this client and its assigned pipe server task. |
90 | | /// |
91 | | /// Populated at [`Client`] construction; cloned by the pipe server task upon |
92 | | /// successful PID correlation and consulted during input forwarding to |
93 | | /// determine whether records should be sent to the client. |
94 | | pipe_server_state: Arc<Mutex<PipeServerState>>, |
95 | | } |
96 | | |
97 | | unsafe impl Send for Client {} |
98 | | |
99 | | /// Collection of [`Client`]s maintaining insertion order and a PID-indexed |
100 | | /// lookup table. |
101 | | /// |
102 | | /// The ordered list preserves client window placement semantics, while the |
103 | | /// index enables O(1) lookup by process id — required by the pipe server task |
104 | | /// during PID correlation and future per-client pipe server control. |
105 | | struct Clients { |
106 | | /// Ordered list of clients; order matches launch order and is used for |
107 | | /// window arrangement and z-order synchronization. |
108 | | list: Vec<Client>, |
109 | | /// Maps a client's process id to its index in [`list`](Clients::list). |
110 | | pid_index: HashMap<u32, usize>, |
111 | | } |
112 | | |
113 | | impl Clients { |
114 | | /// Creates a new empty collection. |
115 | 6 | fn new() -> Self { |
116 | 6 | return Clients { |
117 | 6 | list: Vec::new(), |
118 | 6 | pid_index: HashMap::new(), |
119 | 6 | }; |
120 | 6 | } |
121 | | |
122 | | /// Appends a client to the collection and records its position in the |
123 | | /// PID index. |
124 | | /// |
125 | | /// # Arguments |
126 | | /// |
127 | | /// * `client` - The [`Client`] to add. |
128 | | /// |
129 | | /// # Panics |
130 | | /// |
131 | | /// Panics if a client with the same process id is already present, as |
132 | | /// duplicate PIDs indicate broken daemon bookkeeping. |
133 | 9 | fn push(&mut self, client: Client) { |
134 | 9 | let index = self.list.len(); |
135 | 9 | assert!( |
136 | 9 | !self.pid_index.contains_key(&client.process_id), |
137 | | "Duplicate client PID {} — daemon bookkeeping broken", |
138 | | client.process_id, |
139 | | ); |
140 | 8 | self.pid_index.insert(client.process_id, index); |
141 | 8 | self.list.push(client); |
142 | 8 | } |
143 | | |
144 | | /// Returns a reference to the client with the given process id, if any. |
145 | | /// |
146 | | /// # Arguments |
147 | | /// |
148 | | /// * `pid` - The process id of the client to look up. |
149 | | /// |
150 | | /// # Returns |
151 | | /// |
152 | | /// `Some(&Client)` if a client with the given PID exists, `None` otherwise. |
153 | 10 | fn get_by_pid(&self, pid: u32) -> Option<&Client> { |
154 | 10 | return self |
155 | 10 | .pid_index |
156 | 10 | .get(&pid) |
157 | 10 | .map(|&index| return &self.list[index]7 ); |
158 | 10 | } |
159 | | |
160 | | /// Retains only the clients for which the predicate returns `true`, |
161 | | /// rebuilding the PID index to reflect the new positions. |
162 | | /// |
163 | | /// # Arguments |
164 | | /// |
165 | | /// * `f` - Predicate applied to each [`Client`]; kept when it returns `true`. |
166 | 1 | fn retain<F: FnMut(&Client) -> bool>(&mut self, mut f: F) { |
167 | 3 | self.list1 .retain1 (|client| return f(client)); |
168 | 1 | self.pid_index.clear(); |
169 | 2 | for (index, client) in self.list.iter()1 .enumerate1 () { |
170 | 2 | self.pid_index.insert(client.process_id, index); |
171 | 2 | } |
172 | 1 | } |
173 | | } |
174 | | |
175 | | /// Allows treating a [`Clients`] collection as a `&[Client]`, so callers can |
176 | | /// use `&clients` where a slice is expected and get slice methods |
177 | | /// (`iter`, `len`, `is_empty`, ...) via deref coercion. |
178 | | impl std::ops::Deref for Clients { |
179 | | type Target = [Client]; |
180 | | |
181 | 7 | fn deref(&self) -> &[Client] { |
182 | 7 | return &self.list; |
183 | 7 | } |
184 | | } |
185 | | |
186 | | /// Consumes the collection and yields its clients in insertion order. |
187 | | /// |
188 | | /// Used when merging a freshly launched [`Clients`] batch into an existing |
189 | | /// collection while also spawning per-client pipe servers. |
190 | | impl IntoIterator for Clients { |
191 | | type Item = Client; |
192 | | type IntoIter = std::vec::IntoIter<Client>; |
193 | | |
194 | 0 | fn into_iter(self) -> Self::IntoIter { |
195 | 0 | return self.list.into_iter(); |
196 | 0 | } |
197 | | } |
198 | | |
199 | | /// Hacky wrapper around a window handle. |
200 | | /// |
201 | | /// As we cannot implement foreign traits for foreign structs |
202 | | /// we introduce this wrapper to implement [Send] for [HWND]. |
203 | | #[derive(Debug, Eq)] |
204 | | struct HWNDWrapper { |
205 | | hwdn: HWND, |
206 | | } |
207 | | |
208 | | unsafe impl Send for HWNDWrapper {} |
209 | | |
210 | | impl PartialEq for HWNDWrapper { |
211 | | /// Returns whether to `HWNDWrapper` instances are equal or not |
212 | | /// based on the [HWND] they wrap. |
213 | 2 | fn eq(&self, other: &Self) -> bool { |
214 | 2 | return self.hwdn == other.hwdn; |
215 | 2 | } |
216 | | } |
217 | | |
218 | | /// Returns a window handle to the current console window. |
219 | | /// |
220 | | /// The [HWND] is wrapped in a `HWNDWrapper` so that |
221 | | /// we can pass it inbetween threads. |
222 | 0 | fn get_console_window_wrapper(api: &dyn WindowsApi) -> HWNDWrapper { |
223 | 0 | return HWNDWrapper { |
224 | 0 | hwdn: api.get_console_window(), |
225 | 0 | }; |
226 | 0 | } |
227 | | |
228 | | /// Returns a window handle to the foreground window. |
229 | | /// |
230 | | /// The [HWND] is wrapped in a `HWNDWrapper` so that |
231 | | /// we can pass it inbetween threads. |
232 | 0 | fn get_foreground_window_wrapper(api: &dyn WindowsApi) -> HWNDWrapper { |
233 | 0 | return HWNDWrapper { |
234 | 0 | hwdn: api.get_foreground_window(), |
235 | 0 | }; |
236 | 0 | } |
237 | | |
238 | | /// Enum of all possible control mode states. |
239 | | #[derive(PartialEq, Debug)] |
240 | | enum ControlModeState { |
241 | | /// Controle mode is inactive. |
242 | | Inactive, |
243 | | /// One of the keys required for the control mode key combination |
244 | | /// is currently being pressed. |
245 | | Initiated, |
246 | | /// All required keys for the control mode key combination were pressed |
247 | | /// and control mode is now active. |
248 | | /// |
249 | | /// Active control mode prevents any input records from being sent to clients. |
250 | | Active, |
251 | | } |
252 | | |
253 | | /// The daemon is responsible to launch a client for |
254 | | /// each host, positioning the client windows, forwarding |
255 | | /// input records to all clients and handling control mode. |
256 | | struct Daemon<'a> { |
257 | | /// A list of hostnames to connect to. |
258 | | hosts: Vec<String>, |
259 | | /// A username to use to connect to all clients. |
260 | | /// |
261 | | /// If it is empty the clients will use the SSH config to find an approriate |
262 | | /// username. |
263 | | username: Option<String>, |
264 | | /// Optional port used for all SSH connections. |
265 | | port: Option<u16>, |
266 | | /// The `DaemonConfig` that controls how the daemon console window looks like. |
267 | | config: &'a DaemonConfig, |
268 | | /// List of available cluster tags |
269 | | clusters: &'a [Cluster], |
270 | | /// The current control mode state. |
271 | | control_mode_state: ControlModeState, |
272 | | /// If debug mode is enabled on the daemon it will also be enabled on all |
273 | | /// clients. |
274 | | debug: bool, |
275 | | } |
276 | | |
277 | | impl<'a> Daemon<'a> { |
278 | | /// Launches all client windows and blocks on the main run loop. |
279 | | /// |
280 | | /// Sets up the daemon console by disabling processed input mode and applying |
281 | | /// the configured colors and dimensions. |
282 | | /// Once all client windows have successfully started the daemon console window |
283 | | /// is moved to the foreground and receives focus. |
284 | 0 | async fn launch<W: WindowsApi + Clone + 'static>(mut self, windows_api: &W) { |
285 | 0 | windows_api |
286 | 0 | .set_console_title(format!("{PKG_NAME} daemon").as_str()) |
287 | 0 | .unwrap(); |
288 | 0 | set_console_color( |
289 | 0 | windows_api, |
290 | 0 | CONSOLE_CHARACTER_ATTRIBUTES(self.config.console_color), |
291 | | ); |
292 | 0 | set_console_border_color(windows_api, COLORREF(0x000000FF)); |
293 | | |
294 | 0 | toggle_processed_input_mode(windows_api); // Disable processed input mode |
295 | | |
296 | | // Initialize the COM library so we can use UI automation |
297 | 0 | windows_api |
298 | 0 | .initialize_com_library(windows::Win32::System::Com::COINIT_MULTITHREADED) |
299 | 0 | .unwrap(); |
300 | | |
301 | 0 | let workspace_area = workspace::get_workspace_area(windows_api, self.config.height); |
302 | | |
303 | 0 | self.arrange_daemon_console(windows_api, &workspace_area); |
304 | | |
305 | | // Looks like on windows 10 re-arranging the console resets the console output buffer |
306 | 0 | set_console_color( |
307 | 0 | windows_api, |
308 | 0 | CONSOLE_CHARACTER_ATTRIBUTES(self.config.console_color), |
309 | | ); |
310 | | |
311 | 0 | let mut clients = Arc::new(Mutex::new( |
312 | 0 | launch_clients( |
313 | 0 | windows_api, |
314 | 0 | self.hosts.to_vec(), |
315 | 0 | &self.username, |
316 | 0 | self.port, |
317 | 0 | self.debug, |
318 | 0 | &workspace_area, |
319 | 0 | self.config.aspect_ratio_adjustement, |
320 | 0 | 0, |
321 | 0 | ) |
322 | 0 | .await, |
323 | | )); |
324 | | |
325 | | // Now that all clients started, focus the daemon console again. |
326 | 0 | let daemon_console = windows_api.get_console_window(); |
327 | 0 | let _ = windows_api.set_foreground_window(daemon_console); |
328 | 0 | let _ = windows_api.focus_window_with_automation(daemon_console); |
329 | | |
330 | 0 | self.print_instructions(windows_api); |
331 | 0 | self.run(windows_api, &mut clients, &workspace_area).await; |
332 | 0 | } |
333 | | |
334 | | /// The main run loop of the `daemon` subcommand. |
335 | | /// |
336 | | /// Opens a multi-producer, multi-consumer broadcasting channel used to |
337 | | /// send the read input records in parallel to the name pipe servers |
338 | | /// the clients are listening on. |
339 | | /// Spawns a background thread that waits for all clients to terminate |
340 | | /// and then stops the current process. |
341 | | /// Spawns a background thread that ensures the z-order of all client |
342 | | /// windows is in sync with the daemon window. |
343 | | /// I.e. if the daemon window is focussed, all clients should be moved to the foreground. |
344 | | /// |
345 | | /// The main loop consists of waiting for input records to read from the keyboard, |
346 | | /// sending them to all clients and handling control mode. |
347 | | /// |
348 | | /// # Arguments |
349 | | /// |
350 | | /// * `windows_api` - The Windows API implementation to use |
351 | | /// * `clients` - A thread safe mapping from the number |
352 | | /// a client console window was launched at |
353 | | /// in relation to the other client windows |
354 | | /// and the clients console window handle. |
355 | | /// * `workspace_area` - The available workspace area on the |
356 | | /// primary monitor minus the space occupied |
357 | | /// by the daemon console window. |
358 | 0 | async fn run<W: WindowsApi + Clone + 'static>( |
359 | 0 | &mut self, |
360 | 0 | windows_api: &W, |
361 | 0 | clients: &mut Arc<Mutex<Clients>>, |
362 | 0 | workspace_area: &workspace::WorkspaceArea, |
363 | 0 | ) { |
364 | 0 | let (sender, _) = |
365 | 0 | broadcast::channel::<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>(SENDER_CAPACITY); |
366 | | |
367 | 0 | let mut servers = Arc::new(Mutex::new( |
368 | 0 | self.launch_named_pipe_servers(&sender, Arc::clone(clients)), |
369 | | )); |
370 | | |
371 | | // Monitor client processes |
372 | 0 | let clients_clone = Arc::clone(clients); |
373 | 0 | let windows_api_clone = windows_api.clone(); |
374 | 0 | tokio::spawn(async move { |
375 | | loop { |
376 | 0 | clients_clone.lock().unwrap().retain(|client| { |
377 | 0 | match windows_api_clone.get_exit_code(client.process_handle) { |
378 | 0 | Ok(exit_code) => return exit_code == STILL_ACTIVE.0 as u32, |
379 | 0 | Err(_) => return false, // Process handle is invalid, remove client |
380 | | } |
381 | 0 | }); |
382 | 0 | if clients_clone.lock().unwrap().is_empty() { |
383 | | // All clients have exited, exit the daemon as well |
384 | 0 | std::process::exit(0); |
385 | 0 | } |
386 | 0 | tokio::time::sleep(Duration::from_millis(5)).await; |
387 | | } |
388 | | }); |
389 | | |
390 | 0 | ensure_client_z_order_in_sync_with_daemon( |
391 | 0 | Arc::new(windows_api.clone()), |
392 | 0 | clients.to_owned(), |
393 | | ); |
394 | | |
395 | | loop { |
396 | 0 | self.handle_input_record( |
397 | 0 | windows_api, |
398 | 0 | &sender, |
399 | 0 | read_keyboard_input(windows_api), |
400 | 0 | clients, |
401 | 0 | workspace_area, |
402 | 0 | &mut servers, |
403 | 0 | ) |
404 | 0 | .await; |
405 | | } |
406 | | } |
407 | | |
408 | | /// Launch a named pipe server for each host in a dedicated thread. |
409 | | /// |
410 | | /// # Arguments |
411 | | /// |
412 | | /// * `sender` - The sender end of the broadcast channel through which |
413 | | /// the main thread will send the input records that are to |
414 | | /// be forwarded to the clients. |
415 | | /// |
416 | | /// # Returns |
417 | | /// |
418 | | /// Returns a list of [JoinHandle]s, one handle for each thread. |
419 | 0 | fn launch_named_pipe_servers( |
420 | 0 | &self, |
421 | 0 | sender: &Sender<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>, |
422 | 0 | clients: Arc<Mutex<Clients>>, |
423 | 0 | ) -> Vec<JoinHandle<()>> { |
424 | 0 | let mut servers: Vec<JoinHandle<()>> = Vec::new(); |
425 | 0 | for _ in &self.hosts { |
426 | 0 | self.launch_named_pipe_server(&mut servers, sender, Arc::clone(&clients)); |
427 | 0 | } |
428 | 0 | return servers; |
429 | 0 | } |
430 | | |
431 | | /// Launch a named pipe server in a dedicated thread. |
432 | | /// |
433 | | /// # Arguments |
434 | | /// |
435 | | /// * `servers` - A list of [JoinHandle]s to which the join handle for |
436 | | /// the new thread will be added. |
437 | | /// * `sender` - The sender end of the broadcast channel through which |
438 | | /// the main thread will send the input records that are to |
439 | | /// be forwarded to the clients. |
440 | 0 | fn launch_named_pipe_server( |
441 | 0 | &self, |
442 | 0 | servers: &mut Vec<JoinHandle<()>>, |
443 | 0 | sender: &Sender<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>, |
444 | 0 | clients: Arc<Mutex<Clients>>, |
445 | 0 | ) { |
446 | 0 | let named_pipe_server = ServerOptions::new() |
447 | 0 | .access_inbound(true) |
448 | 0 | .access_outbound(true) |
449 | 0 | .pipe_mode(PipeMode::Message) |
450 | 0 | .create(PIPE_NAME) |
451 | 0 | .unwrap_or_else(|err| { |
452 | 0 | error!("{}", err); |
453 | 0 | panic!("Failed to create named pipe server",) |
454 | | }); |
455 | 0 | let mut receiver = sender.subscribe(); |
456 | 0 | servers.push(tokio::spawn(async move { |
457 | 0 | named_pipe_server_routine(named_pipe_server, &mut receiver, clients).await; |
458 | 0 | })); |
459 | 0 | } |
460 | | |
461 | | /// Handle the given input record. |
462 | | /// |
463 | | /// Input records are being forwarded to all clients. |
464 | | /// If a sequence of input records matches the control mode |
465 | | /// key combination, forwarding is temporarily interrupted, |
466 | | /// until control mode is exited. |
467 | | /// |
468 | | /// # Arguments |
469 | | /// |
470 | | /// * `sender` - The sender end of the broadcast channel |
471 | | /// through which we will send the input records |
472 | | /// that are being forwarded to the clients |
473 | | /// by the named pipe servers (`servers`). |
474 | | /// * `input_record` - The [INPUT_RECORD_0].`KeyEvent` read from the |
475 | | /// console input buffer. |
476 | | /// * `clients` - A thread safe mapping from the number |
477 | | /// a client console window was launched at |
478 | | /// in relation to the other client windows |
479 | | /// and the clients console window handle. |
480 | | /// The mapping will be extended if additional clients |
481 | | /// are being added through control mode `[c]reate window(s)`. |
482 | | /// * `workspace_area` - The available workspace area on the |
483 | | /// primary monitor minus the space occupied |
484 | | /// by the daemon console window. |
485 | | /// * `servers` - A thread safe list of [JoinHandle]s, |
486 | | /// one handle for each named pipe server background thread. |
487 | | /// The list will be extended if additional clients are being added |
488 | | /// through control mode `[c]reate window(s)`. |
489 | 0 | async fn handle_input_record<W: WindowsApi + Clone + 'static>( |
490 | 0 | &mut self, |
491 | 0 | windows_api: &W, |
492 | 0 | sender: &Sender<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>, |
493 | 0 | input_record: INPUT_RECORD_0, |
494 | 0 | clients: &mut Arc<Mutex<Clients>>, |
495 | 0 | workspace_area: &workspace::WorkspaceArea, |
496 | 0 | servers: &mut Arc<Mutex<Vec<JoinHandle<()>>>>, |
497 | 0 | ) { |
498 | 0 | if self.control_mode_is_active(windows_api, input_record) { |
499 | 0 | if self.control_mode_state == ControlModeState::Initiated { |
500 | 0 | clear_screen(windows_api); |
501 | 0 | println!("Control Mode (Esc to exit)"); |
502 | 0 | println!("[c]reate window(s), [r]etile, copy active [h]ostname(s)"); |
503 | 0 | self.control_mode_state = ControlModeState::Active; |
504 | 0 | return; |
505 | 0 | } |
506 | 0 | let key_event = unsafe { input_record.KeyEvent }; |
507 | 0 | if !key_event.bKeyDown.as_bool() { |
508 | 0 | return; |
509 | 0 | } |
510 | 0 | match ( |
511 | 0 | VIRTUAL_KEY(key_event.wVirtualKeyCode), |
512 | 0 | key_event.dwControlKeyState, |
513 | 0 | ) { |
514 | 0 | (VK_R, 0) => { |
515 | 0 | self.rearrange_client_windows( |
516 | 0 | windows_api, |
517 | 0 | &clients.lock().unwrap(), |
518 | 0 | workspace_area, |
519 | 0 | ); |
520 | 0 | self.arrange_daemon_console(windows_api, workspace_area); |
521 | 0 | } |
522 | 0 | (VK_E, 0) => { |
523 | 0 | // TODO: Select windows |
524 | 0 | } |
525 | 0 | (VK_T, 0) => { |
526 | 0 | // TODO: trigger input on selected windows |
527 | 0 | } |
528 | | (VK_C, 0) => { |
529 | 0 | clear_screen(windows_api); |
530 | | // TODO: make ESC abort |
531 | 0 | println!("Hostname(s) or cluster tag(s): (leave empty to abort)"); |
532 | 0 | toggle_processed_input_mode(windows_api); // As it was disabled before, this enables it again |
533 | 0 | let mut hostnames = String::new(); |
534 | 0 | match io::stdin().read_line(&mut hostnames) { |
535 | 0 | Ok(2) => { |
536 | 0 | // Empty input (only newline '\n') |
537 | 0 | } |
538 | | Ok(_) => { |
539 | 0 | let number_of_existing_clients = clients.lock().unwrap().len(); |
540 | 0 | let new_clients = launch_clients( |
541 | 0 | windows_api, |
542 | 0 | resolve_cluster_tags( |
543 | 0 | hostnames.split(' ').map(|x| return x.trim()).collect(), |
544 | 0 | self.clusters, |
545 | | ) |
546 | 0 | .into_iter() |
547 | 0 | .map(|x| return x.to_owned()) |
548 | 0 | .collect(), |
549 | 0 | &self.username, |
550 | 0 | self.port, |
551 | 0 | self.debug, |
552 | 0 | workspace_area, |
553 | 0 | self.config.aspect_ratio_adjustement, |
554 | 0 | number_of_existing_clients, |
555 | | ) |
556 | 0 | .await; |
557 | 0 | for client in new_clients.into_iter() { |
558 | 0 | clients.lock().unwrap().push(client); |
559 | 0 | self.launch_named_pipe_server( |
560 | 0 | &mut servers.lock().unwrap(), |
561 | 0 | sender, |
562 | 0 | Arc::clone(clients), |
563 | 0 | ); |
564 | 0 | } |
565 | | } |
566 | 0 | Err(error) => { |
567 | 0 | error!("{error}"); |
568 | | } |
569 | | } |
570 | 0 | toggle_processed_input_mode(windows_api); // Re-disable processed input mode. |
571 | 0 | self.rearrange_client_windows( |
572 | 0 | windows_api, |
573 | 0 | &clients.lock().unwrap(), |
574 | 0 | workspace_area, |
575 | | ); |
576 | 0 | self.arrange_daemon_console(windows_api, workspace_area); |
577 | | // Focus the daemon console again. |
578 | 0 | let daemon_window = windows_api.get_console_window(); |
579 | 0 | let _ = windows_api.set_foreground_window(daemon_window); |
580 | 0 | let _ = windows_api.focus_window_with_automation(daemon_window); |
581 | 0 | self.quit_control_mode(windows_api); |
582 | | } |
583 | | (VK_H, 0) => { |
584 | 0 | let mut active_hostnames: Vec<String> = vec![]; |
585 | 0 | for client in clients.lock().unwrap().iter() { |
586 | 0 | if windows_api.is_window(client.window_handle) { |
587 | 0 | active_hostnames.push(client.hostname.clone()); |
588 | 0 | } |
589 | | } |
590 | 0 | cli_clipboard::set_contents(active_hostnames.join(" ")).unwrap(); |
591 | 0 | self.quit_control_mode(windows_api); |
592 | | } |
593 | 0 | _ => {} |
594 | | } |
595 | 0 | return; |
596 | 0 | } |
597 | 0 | let error_handler = |err| { |
598 | 0 | error!("{}", err); |
599 | 0 | panic!( |
600 | | "Failed to serialize input recored `{}`", |
601 | 0 | input_record.string_repr() |
602 | | ) |
603 | | }; |
604 | 0 | match sender.send( |
605 | 0 | serialize_input_record_0(&input_record)[..] |
606 | 0 | .try_into() |
607 | 0 | .unwrap_or_else(error_handler), |
608 | 0 | ) { |
609 | 0 | Ok(_) => {} |
610 | 0 | Err(_) => { |
611 | 0 | thread::sleep(time::Duration::from_nanos(1)); |
612 | 0 | } |
613 | | } |
614 | 0 | } |
615 | | |
616 | | /// Returns whether control mode is active or not given the input_record. |
617 | | /// |
618 | | /// For control mode to be active this function needs to be called |
619 | | /// multiple times, as a key press translates to an input record and |
620 | | /// the key combination that activates control mode has 2 keys: |
621 | | /// `Ctrl + A`. |
622 | | /// The current control mode state is stored in `self.control_mode_state`. |
623 | | /// |
624 | | /// # Arguments |
625 | | /// |
626 | | /// * `windows_api` - The Windows API implementation to use |
627 | | /// * `input_record` - A KeyEvent input record. |
628 | | /// |
629 | | /// # Returns |
630 | | /// |
631 | | /// Whether or not control mode is active. |
632 | 0 | fn control_mode_is_active<W: WindowsApi>( |
633 | 0 | &mut self, |
634 | 0 | windows_api: &W, |
635 | 0 | input_record: INPUT_RECORD_0, |
636 | 0 | ) -> bool { |
637 | 0 | let key_event = unsafe { input_record.KeyEvent }; |
638 | 0 | if self.control_mode_state == ControlModeState::Active { |
639 | 0 | if key_event.wVirtualKeyCode == VK_ESCAPE.0 { |
640 | 0 | self.quit_control_mode(windows_api); |
641 | 0 | return false; |
642 | 0 | } |
643 | 0 | return true; |
644 | 0 | } |
645 | 0 | if (key_event.dwControlKeyState & LEFT_CTRL_PRESSED >= 1 |
646 | 0 | || key_event.dwControlKeyState & RIGHT_CTRL_PRESSED >= 1) |
647 | 0 | && key_event.wVirtualKeyCode == VK_A.0 |
648 | | { |
649 | 0 | self.control_mode_state = ControlModeState::Initiated; |
650 | 0 | return true; |
651 | 0 | } |
652 | 0 | return false; |
653 | 0 | } |
654 | | |
655 | | /// Prints the default daemon instructions to the daemon console and |
656 | | /// sets `self.control_mode_state` to inactive. |
657 | 0 | fn quit_control_mode<W: WindowsApi>(&mut self, windows_api: &W) { |
658 | 0 | self.print_instructions(windows_api); |
659 | 0 | self.control_mode_state = ControlModeState::Inactive; |
660 | 0 | } |
661 | | |
662 | | /// Clears the console screen and prints the default daemon instructions. |
663 | 0 | fn print_instructions<W: WindowsApi>(&self, windows_api: &W) { |
664 | 0 | clear_screen(windows_api); |
665 | 0 | println!("Input to terminal: (Ctrl-A to enter control mode)"); |
666 | 0 | } |
667 | | |
668 | | /// Iterates over all still open client windows and re-arranges them |
669 | | /// on the screen based on the aspect ration adjustment daemon configuration. |
670 | | /// |
671 | | /// Client windows will be re-sized and re-positioned. |
672 | | /// |
673 | | /// # Arguments |
674 | | /// |
675 | | /// * `windows_api` - The Windows API implementation to use |
676 | | /// * `clients` - A thread safe mapping from the number |
677 | | /// a client console window was launched at |
678 | | /// in relation to the other client windows |
679 | | /// and the clients console window handle. |
680 | | /// The number is relevant to determine the |
681 | | /// position on the screen the window should |
682 | | /// be placed at. |
683 | | /// * `workspace_area` - The available workspace area on the |
684 | | /// primary monitor minus the space occupied |
685 | | /// by the daemon console window. |
686 | 0 | fn rearrange_client_windows<W: WindowsApi>( |
687 | 0 | &self, |
688 | 0 | windows_api: &W, |
689 | 0 | clients: &[Client], |
690 | 0 | workspace_area: &workspace::WorkspaceArea, |
691 | 0 | ) { |
692 | 0 | let mut valid_clients = Vec::new(); |
693 | 0 | for client in clients.iter() { |
694 | 0 | let exit_code = match windows_api.get_exit_code(client.process_handle) { |
695 | 0 | Ok(code) => code, |
696 | 0 | Err(_) => continue, // Process handle is invalid, skip client |
697 | | }; |
698 | 0 | if exit_code == STILL_ACTIVE.0 as u32 && windows_api.is_window(client.window_handle) { |
699 | 0 | valid_clients.push(client); |
700 | 0 | } |
701 | | } |
702 | 0 | for (index, client) in valid_clients.iter().enumerate() { |
703 | 0 | arrange_client_window( |
704 | 0 | windows_api, |
705 | 0 | &client.window_handle, |
706 | 0 | workspace_area, |
707 | 0 | index, |
708 | 0 | valid_clients.len(), |
709 | 0 | self.config.aspect_ratio_adjustement, |
710 | | ) |
711 | | } |
712 | 0 | } |
713 | | |
714 | | /// Re-sizes and re-positions the daemon console window on the screen |
715 | | /// based on the daemon height configuration. |
716 | | /// |
717 | | /// # Arguments |
718 | | /// |
719 | | /// * `windows_api` - The Windows API implementation to use |
720 | | /// * `workspace_area` - The available workspace area on the |
721 | | /// primary monitor minus the space occupied |
722 | | /// by the daemon console window. |
723 | 0 | fn arrange_daemon_console<W: WindowsApi>( |
724 | 0 | &self, |
725 | 0 | windows_api: &W, |
726 | 0 | workspace_area: &WorkspaceArea, |
727 | 0 | ) { |
728 | 0 | let (x, y, width, height) = get_console_rect( |
729 | 0 | 0, |
730 | 0 | workspace_area.height, |
731 | 0 | workspace_area.width - (workspace_area.x_fixed_frame + workspace_area.x_size_frame), |
732 | 0 | self.config.height, |
733 | 0 | workspace_area, |
734 | 0 | ); |
735 | 0 | arrange_console(windows_api, x, y, width, height); |
736 | 0 | } |
737 | | } |
738 | | |
739 | | /// The processed console input mode controls whether special key combinations |
740 | | /// such as `Ctrl + c` or `Ctrl + BREAK` receive special handling or are treated |
741 | | /// as simple key presses. |
742 | | /// |
743 | | /// By default processed input mode is enabled, meaning `Ctrl + c` is treated as |
744 | | /// a signal, not key presses. |
745 | | /// |
746 | | /// <https://learn.microsoft.com/en-us/windows/console/ctrl-c-and-ctrl-break-signals> |
747 | | /// |
748 | | /// # Arguments |
749 | | /// |
750 | | /// * `windows_api` - The Windows API implementation to use |
751 | 0 | fn toggle_processed_input_mode<W: WindowsApi>(windows_api: &W) { |
752 | 0 | let handle = get_console_input_buffer(); |
753 | 0 | let mode = windows_api.get_console_mode(handle).unwrap(); |
754 | 0 | let new_mode = windows::Win32::System::Console::CONSOLE_MODE(mode.0 ^ ENABLE_PROCESSED_INPUT.0); |
755 | 0 | let _ = windows_api.set_console_mode(handle, new_mode); |
756 | 0 | } |
757 | | |
758 | | /// Resolve cluster tags into hostnames |
759 | | /// |
760 | | /// Iterates over the list of hosts to find and resolve cluster tags. |
761 | | /// Nested cluster tags are supported but recursivness is not checked for. |
762 | | /// |
763 | | /// # Arguments |
764 | | /// |
765 | | /// * `hosts` - List of hosts including hostnames and or cluster tags |
766 | | /// * `clusters` - List of available cluster tags |
767 | | /// |
768 | | /// # Returns |
769 | | /// |
770 | | /// A list of hostnames |
771 | 12 | pub fn resolve_cluster_tags<'a>(hosts: Vec<&'a str>, clusters: &'a [Cluster]) -> Vec<&'a str> { |
772 | 12 | let mut resolved_hosts: Vec<&str> = Vec::new(); |
773 | | let mut is_cluster_tag: bool; |
774 | 22 | for host in hosts12 { |
775 | 22 | is_cluster_tag = false; |
776 | 22 | for cluster17 in clusters { |
777 | 17 | if host == cluster.name { |
778 | 3 | is_cluster_tag = true; |
779 | 3 | resolved_hosts.extend(resolve_cluster_tags( |
780 | 6 | cluster.hosts.iter()3 .map3 (|host| return &**host).collect3 (), |
781 | 3 | clusters, |
782 | | )); |
783 | 3 | break; |
784 | 14 | } |
785 | | } |
786 | 22 | if !is_cluster_tag { |
787 | 19 | resolved_hosts.push(host); |
788 | 19 | }3 |
789 | | } |
790 | 12 | return resolved_hosts; |
791 | 12 | } |
792 | | |
793 | | /// Launches a client console for each given host and waits for |
794 | | /// the client windows to exist before returning their handles. |
795 | | /// |
796 | | /// # Arguments |
797 | | /// |
798 | | /// * `windows_api` - The Windows API implementation to use |
799 | | /// * `hosts` - List of hosts |
800 | | /// * `username` - Optional username, if none is given |
801 | | /// the client will use the SSH config to |
802 | | /// determine a username. |
803 | | /// * `port` - Optional port for SSH connections |
804 | | /// * `debug` - Toggles debug mode on the client. |
805 | | /// * `workspace_area` - The available workspace area on the primary monitor |
806 | | /// minus the space occupied by the daemon console window. |
807 | | /// Used to arrange the client window. |
808 | | /// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration. |
809 | | /// Used to arrange the client window. |
810 | | /// * `index_offset` - Offset used to position the new windows correctly |
811 | | /// from the start, avoiding flickering. |
812 | | /// |
813 | | /// # Returns |
814 | | /// |
815 | | /// A [`Clients`] collection preserving the launch order and indexed by |
816 | | /// process id for pipe-server correlation. |
817 | 0 | async fn launch_clients<W: WindowsApi + 'static + Clone>( |
818 | 0 | windows_api: &W, |
819 | 0 | hosts: Vec<String>, |
820 | 0 | username: &Option<String>, |
821 | 0 | port: Option<u16>, |
822 | 0 | debug: bool, |
823 | 0 | workspace_area: &workspace::WorkspaceArea, |
824 | 0 | aspect_ratio_adjustment: f64, |
825 | 0 | index_offset: usize, |
826 | 0 | ) -> Clients { |
827 | 0 | let len_hosts = hosts.len(); |
828 | 0 | let _guard = WindowsSettingsDefaultTerminalApplicationGuard::new(); |
829 | | |
830 | | // Create an Arc to share the windows_api across parallel tasks |
831 | 0 | let windows_api_arc = Arc::new(windows_api.clone()); |
832 | | |
833 | | // Create tasks for each client launch using spawn_blocking to handle the synchronous operations |
834 | 0 | let mut tasks = Vec::new(); |
835 | | |
836 | 0 | for (index, host) in hosts.into_iter().enumerate() { |
837 | 0 | let username_client = username.clone(); |
838 | 0 | let workspace_area_client = *workspace_area; |
839 | 0 | let windows_api_clone = Arc::clone(&windows_api_arc); |
840 | | |
841 | | // Use spawn_blocking to run the synchronous launch_client_console in parallel |
842 | 0 | let task = tokio::task::spawn_blocking(move || { |
843 | 0 | let (window_handle, process_handle, process_id) = launch_client_console( |
844 | 0 | windows_api_clone.as_ref(), |
845 | 0 | &host, |
846 | 0 | username_client, |
847 | 0 | port, |
848 | 0 | debug, |
849 | 0 | index + index_offset, |
850 | 0 | &workspace_area_client, |
851 | 0 | len_hosts + index_offset, |
852 | 0 | aspect_ratio_adjustment, |
853 | 0 | ); |
854 | 0 | return ( |
855 | 0 | index, |
856 | 0 | Client { |
857 | 0 | hostname: host, |
858 | 0 | window_handle, |
859 | 0 | process_handle, |
860 | 0 | process_id, |
861 | 0 | pipe_server_state: Arc::new(Mutex::new(PipeServerState::Enabled)), |
862 | 0 | }, |
863 | 0 | ); |
864 | 0 | }); |
865 | | |
866 | 0 | tasks.push(task); |
867 | | } |
868 | | |
869 | | // Wait for all tasks to complete in parallel |
870 | 0 | let mut results = Vec::new(); |
871 | 0 | for task in tasks { |
872 | 0 | match task.await { |
873 | 0 | Ok(result) => results.push(result), |
874 | 0 | Err(e) => panic!("Failed to launch client: {e}"), |
875 | | } |
876 | | } |
877 | | |
878 | | // Sort results by index to maintain order |
879 | 0 | results.sort_by_key(|(index, _)| return *index); |
880 | | |
881 | 0 | let mut clients = Clients::new(); |
882 | 0 | for (_, client) in results.into_iter() { |
883 | 0 | clients.push(client); |
884 | 0 | } |
885 | 0 | return clients; |
886 | 0 | } |
887 | | |
888 | | /// Launchs a `client` console process with its own window with the given |
889 | | /// CLI arguments/options: `host`, `username`, `port`, `debug`. |
890 | | /// |
891 | | /// Waits for the window to open, then re-arranges it based on |
892 | | /// the total number of clients, the size of the daemon console window and |
893 | | /// its index relative to the other client windows. |
894 | | /// |
895 | | /// # Arguments |
896 | | /// |
897 | | /// * `windows_api` - The Windows API implementation to use |
898 | | /// * `host` - Hostname the client should connect to |
899 | | /// * `username` - Username the client should use |
900 | | /// * `port` - Optional port for SSH connections |
901 | | /// * `debug` - Toggle debug mode on the client |
902 | | /// * `index` - The index of the client in the list of all clients. |
903 | | /// Used to re-arrange the client window. |
904 | | /// * `workspace_area` - The available workspace area on the primary monitor |
905 | | /// minus the space occupied by the daemon console window. |
906 | | /// * `number_of_consoles` - The total number of active client console windows. |
907 | | /// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration. |
908 | | /// |
909 | | /// # Returns |
910 | | /// |
911 | | /// A tuple containing the window handle, process handle, and process id of the |
912 | | /// client process. |
913 | 0 | fn launch_client_console<W: WindowsApi>( |
914 | 0 | windows_api: &W, |
915 | 0 | host: &str, |
916 | 0 | username: Option<String>, |
917 | 0 | port: Option<u16>, |
918 | 0 | debug: bool, |
919 | 0 | index: usize, |
920 | 0 | workspace_area: &workspace::WorkspaceArea, |
921 | 0 | number_of_consoles: usize, |
922 | 0 | aspect_ratio_adjustment: f64, |
923 | 0 | ) -> (HWND, HANDLE, u32) { |
924 | | // The first argument must be `--` to ensure all following arguments are treated |
925 | | // as positional arguments and not as options if they start with `-`. |
926 | 0 | let mut client_args: Vec<String> = Vec::new(); |
927 | 0 | if debug { |
928 | 0 | client_args.push("-d".to_string()); |
929 | 0 | } |
930 | 0 | let mut actual_host = host; |
931 | 0 | let mut actual_username = username; |
932 | 0 | if let Some(split_result) = host.split_once("@") { |
933 | 0 | actual_username = Some(split_result.0.to_owned()); |
934 | 0 | actual_host = split_result.1; |
935 | 0 | } |
936 | 0 | if let Some(actual_username) = actual_username.as_deref() { |
937 | 0 | client_args.extend(vec!["-u".to_string(), actual_username.to_string()]); |
938 | 0 | } |
939 | 0 | if let Some(port) = port { |
940 | 0 | client_args.extend(vec!["-p".to_string(), port.to_string()]); |
941 | 0 | } |
942 | 0 | client_args.push("client".to_string()); |
943 | 0 | client_args.extend(vec!["--".to_string(), actual_host.to_string()]); |
944 | | |
945 | 0 | let process_info = spawn_console_process(windows_api, &format!("{PKG_NAME}.exe"), client_args) |
946 | 0 | .expect("Failed to create process"); |
947 | 0 | let client_window_handle = get_console_window_handle(windows_api, process_info.dwProcessId); |
948 | 0 | let process_handle = windows_api |
949 | 0 | .open_process(PROCESS_QUERY_INFORMATION.0, false, process_info.dwProcessId) |
950 | 0 | .unwrap_or_else(|err| { |
951 | 0 | panic!( |
952 | | "Failed to open process handle for process {}: {}", |
953 | | process_info.dwProcessId, err |
954 | | ); |
955 | | }); |
956 | | |
957 | 0 | arrange_client_window( |
958 | 0 | windows_api, |
959 | 0 | &client_window_handle, |
960 | 0 | workspace_area, |
961 | 0 | index, |
962 | 0 | number_of_consoles, |
963 | 0 | aspect_ratio_adjustment, |
964 | | ); |
965 | 0 | return ( |
966 | 0 | client_window_handle, |
967 | 0 | process_handle, |
968 | 0 | process_info.dwProcessId, |
969 | 0 | ); |
970 | 0 | } |
971 | | |
972 | | /// Wait for the named pipe server to connect, correlate the client by |
973 | | /// its process id, then forward serialized input records read from the |
974 | | /// broadcast channel to the named pipe server. |
975 | | /// |
976 | | /// Correlation: after [`NamedPipeServer::connect`] resolves, the client is |
977 | | /// expected to write its 4 byte little-endian process id into the pipe. The |
978 | | /// routine looks up the [`Client`] with that PID in the daemon's `clients` |
979 | | /// collection; if it is not found, the routine logs an error and terminates |
980 | | /// the daemon — an unknown PID indicates broken daemon bookkeeping and is |
981 | | /// unrecoverable. |
982 | | /// |
983 | | /// Forwarding: on every broadcast record, the routine matches on the |
984 | | /// [`PipeServerState`] cloned from the correlated client; only |
985 | | /// [`PipeServerState::Enabled`] writes the record to the pipe. The keep-alive |
986 | | /// write stays unconditional so dead pipes are detected regardless of state. |
987 | | /// |
988 | | /// If writing to the pipe fails the pipe is considered closed and the routine ends. |
989 | | /// To detect if a client is still alive even if we are currently |
990 | | /// not sending data, we send a "keep alive packet", |
991 | | /// [`SERIALIZED_INPUT_RECORD_0_LENGTH`] bytes of `1`s. If that fails, the routine ends. |
992 | | /// |
993 | | /// # Arguments |
994 | | /// |
995 | | /// * `server` - The named pipe server over which we send data to the |
996 | | /// client. |
997 | | /// * `receiver` - The receiving end of the broadcast channel through |
998 | | /// which we get the serialize input records from the main |
999 | | /// thread that are to be sent to the client via the named |
1000 | | /// pipe. |
1001 | | /// * `clients` - The daemon's collection of tracked clients, used to |
1002 | | /// correlate the connecting client by PID and to obtain |
1003 | | /// the shared [`PipeServerState`] reference for this server. |
1004 | | /// |
1005 | | /// # Panics |
1006 | | /// |
1007 | | /// Panics if the connecting client sends a PID that is not present in |
1008 | | /// `clients`. |
1009 | 4 | async fn named_pipe_server_routine( |
1010 | 4 | server: NamedPipeServer, |
1011 | 4 | receiver: &mut Receiver<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>, |
1012 | 4 | clients: Arc<Mutex<Clients>>, |
1013 | 4 | ) { |
1014 | | // wait for a client to connect |
1015 | 4 | server.connect().await.unwrap_or_else(|err| {0 |
1016 | 0 | error!("{}", err); |
1017 | 0 | panic!("Timed out waiting for clients to connect to named pipe server",) |
1018 | | }); |
1019 | | |
1020 | | // Correlate the connecting client by reading its 4 byte PID. |
1021 | 4 | let pid3 = read_client_pid(&server).await; |
1022 | 3 | let pipe_server_state2 = match clients.lock().unwrap().get_by_pid(pid) { |
1023 | 2 | Some(client) => Arc::clone(&client.pipe_server_state), |
1024 | | None => { |
1025 | 1 | error!( |
1026 | | "Named pipe server received unknown PID {} — daemon bookkeeping broken", |
1027 | | pid |
1028 | | ); |
1029 | | // In production this exits the daemon; in tests process::exit would kill |
1030 | | // the test runner, so we panic instead so tokio::spawn can catch it. |
1031 | | #[cfg(not(test))] |
1032 | | std::process::exit(1); |
1033 | | #[cfg(test)] |
1034 | 1 | panic!("Unknown client PID {} — daemon bookkeeping broken", pid); |
1035 | | } |
1036 | | }; |
1037 | | |
1038 | | loop { |
1039 | 11 | let ser_input_record8 = match receiver.try_recv() { |
1040 | 8 | Ok(val) => val, |
1041 | | Err(TryRecvError::Empty) => { |
1042 | 2 | tokio::time::sleep(Duration::from_millis(5)).await; |
1043 | | // Try sending dummy data to detect early if the pipe is closed because the client exited |
1044 | 2 | match server.try_write(&[u8::MAX; SERIALIZED_INPUT_RECORD_0_LENGTH]) { |
1045 | 2 | Ok(_) => continue, |
1046 | 0 | Err(e) if e.kind() == io::ErrorKind::WouldBlock => continue, |
1047 | | Err(_) => { |
1048 | 0 | debug!( |
1049 | | "Named pipe server ({:?}) is closed, stopping named pipe server routine", |
1050 | | server |
1051 | | ); |
1052 | 0 | return; |
1053 | | } |
1054 | | } |
1055 | | } |
1056 | 1 | Err(err) => { |
1057 | 1 | error!("{}", err); |
1058 | 1 | panic!("Failed to receive data from the Receiver"); |
1059 | | } |
1060 | | }; |
1061 | | // Only forward to the client if its pipe server state allows it. |
1062 | 8 | match *pipe_server_state.lock().unwrap() { |
1063 | 8 | PipeServerState::Enabled => {} |
1064 | | } |
1065 | | loop { |
1066 | 14 | server.writable().await.unwrap_or_else(|err| {0 |
1067 | 0 | error!("{}", err); |
1068 | 0 | panic!("Timed out waiting for named pipe server to become writable",) |
1069 | | }); |
1070 | 14 | match server.try_write(&ser_input_record) { |
1071 | | Ok(SERIALIZED_INPUT_RECORD_0_LENGTH) => { |
1072 | 7 | debug!("Successfully written all data"); |
1073 | 7 | break; |
1074 | | } |
1075 | 0 | Ok(n) => { |
1076 | | // The data was only written partially, try again |
1077 | 0 | warn!( |
1078 | | "Partially written data, expected {} but only wrote {}", |
1079 | | SERIALIZED_INPUT_RECORD_0_LENGTH, n |
1080 | | ); |
1081 | 0 | continue; |
1082 | | } |
1083 | 7 | Err(e6 ) if e.kind() == io::ErrorKind::WouldBlock6 => { |
1084 | | // Try again |
1085 | 6 | debug!("Writing to named pipe server would have blocked"); |
1086 | 6 | continue; |
1087 | | } |
1088 | | Err(_) => { |
1089 | | // Can happen if the pipe is closed because the |
1090 | | // client exited |
1091 | 1 | debug!( |
1092 | | "Named pipe server ({:?}) is closed, stopping named pipe server routine", |
1093 | | server |
1094 | | ); |
1095 | 1 | return; |
1096 | | } |
1097 | | } |
1098 | | } |
1099 | | } |
1100 | 1 | } |
1101 | | |
1102 | | /// Read the connecting client's 4 byte little-endian process id from the pipe. |
1103 | | /// |
1104 | | /// Reads exactly 4 bytes from `server`, retrying on `WouldBlock`, and decodes |
1105 | | /// them as a `u32`. Any non-recoverable I/O error panics, as a client that |
1106 | | /// cannot send its PID cannot be correlated and forwarding would be |
1107 | | /// impossible. |
1108 | | /// |
1109 | | /// # Arguments |
1110 | | /// |
1111 | | /// * `server` - The connected named pipe server to read from. |
1112 | | /// |
1113 | | /// # Returns |
1114 | | /// |
1115 | | /// The process id sent by the client. |
1116 | | /// |
1117 | | /// # Panics |
1118 | | /// |
1119 | | /// Panics if the pipe is closed before 4 bytes can be read, or if any |
1120 | | /// non-`WouldBlock` I/O error occurs. |
1121 | 4 | async fn read_client_pid(server: &NamedPipeServer) -> u32 { |
1122 | 4 | let mut buf = [0u8; SERIALIZED_PID_LENGTH]; |
1123 | 4 | let mut read = 0usize; |
1124 | 7 | while read < SERIALIZED_PID_LENGTH { |
1125 | 4 | server.readable().await.unwrap_or_else(|err| {0 |
1126 | 0 | panic!("Named pipe server is not readable for PID handshake: {err}") |
1127 | | }); |
1128 | 4 | match server.try_read(&mut buf[read..]) { |
1129 | | Ok(0) => { |
1130 | 1 | panic!("Named pipe server closed before PID handshake completed"); |
1131 | | } |
1132 | 3 | Ok(n) => { |
1133 | 3 | read += n; |
1134 | 3 | } |
1135 | 0 | Err(e) if e.kind() == io::ErrorKind::WouldBlock => { |
1136 | 0 | continue; |
1137 | | } |
1138 | 0 | Err(e) => { |
1139 | 0 | panic!("Failed to read PID from named pipe client: {e}"); |
1140 | | } |
1141 | | } |
1142 | | } |
1143 | 3 | return deserialize_pid(&buf); |
1144 | 3 | } |
1145 | | |
1146 | | /// Re-sizes and re-positions the given client window based on the total number of clients, |
1147 | | /// the size of the daemon console window and its index relative to the other client windows. |
1148 | | /// |
1149 | | /// # Arguments |
1150 | | /// |
1151 | | /// * `windows_api` - The Windows API implementation to use |
1152 | | /// * `handle` - Reference the windows handle of a client console window. |
1153 | | /// * `workspace_area` - The available workspace area on the primary monitor |
1154 | | /// minus the space occupied by the daemon console window. |
1155 | | /// * `index` - The index of the client in the list of all clients. |
1156 | | /// * `number_of_consoles` - The total number of active client console windows. |
1157 | | /// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration. |
1158 | 0 | fn arrange_client_window<W: WindowsApi>( |
1159 | 0 | windows_api: &W, |
1160 | 0 | handle: &HWND, |
1161 | 0 | workspace_area: &workspace::WorkspaceArea, |
1162 | 0 | index: usize, |
1163 | 0 | number_of_consoles: usize, |
1164 | 0 | aspect_ratio_adjustment: f64, |
1165 | 0 | ) { |
1166 | 0 | let (x, y, width, height) = determine_client_spatial_attributes( |
1167 | 0 | index as i32, |
1168 | 0 | number_of_consoles as i32, |
1169 | 0 | workspace_area, |
1170 | 0 | aspect_ratio_adjustment, |
1171 | 0 | ); |
1172 | | // Since windows update 10.0.19041.5072 it can happen that a client windows rendering is broken |
1173 | | // after a move+resize. Why is unclear, but resizing again does solve the issue. |
1174 | | // We first make the window 1 pixel in each dimension too small and imediately fix it. |
1175 | | // To reduce overhead we do not repaint the window the first time. |
1176 | 0 | windows_api |
1177 | 0 | .move_window(*handle, x, y, width - 1, height - 1, false) |
1178 | 0 | .unwrap_or_else(|err| { |
1179 | 0 | error!("{}", err); |
1180 | 0 | panic!("Failed to move window",) |
1181 | | }); |
1182 | 0 | windows_api |
1183 | 0 | .move_window(*handle, x, y, width, height, true) |
1184 | 0 | .unwrap_or_else(|err| { |
1185 | 0 | error!("{}", err); |
1186 | 0 | panic!("Failed to move window",) |
1187 | | }); |
1188 | 0 | } |
1189 | | |
1190 | | /// Calculates the position and dimensions for a client window given its index, |
1191 | | /// the total number of clients and the `aspect_ratio_adjustment` daemon configuration. |
1192 | | /// |
1193 | | /// # Arguments |
1194 | | /// |
1195 | | /// * `index` - The index of the client in the list of all clients. |
1196 | | /// * `number_of_consoles` - The total number of active client console windows. |
1197 | | /// * `workspace_area` - The available workspace area on the primary monitor |
1198 | | /// minus the space occupied by the daemon console window. |
1199 | | /// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration. |
1200 | | /// * `> 0.0` - Aims for vertical rectangle shape. |
1201 | | /// The larger the value, the more exaggerated the "verticality". |
1202 | | /// Eventually the windows will all be columns. |
1203 | | /// * `= 0.0` - Aims for square shape. |
1204 | | /// * `< 0.0` - Aims for horizontal rectangle shape. |
1205 | | /// The smaller the value, the more exaggerated the "horizontality". |
1206 | | /// Eventually the windows will all be rows. |
1207 | | /// `-1.0` is the sweetspot for mostly preserving a 16:9 ratio. |
1208 | 0 | fn determine_client_spatial_attributes( |
1209 | 0 | index: i32, |
1210 | 0 | number_of_consoles: i32, |
1211 | 0 | workspace_area: &workspace::WorkspaceArea, |
1212 | 0 | aspect_ratio_adjustment: f64, |
1213 | 0 | ) -> (i32, i32, i32, i32) { |
1214 | 0 | let aspect_ratio = (workspace_area.width |
1215 | 0 | + (workspace_area.x_fixed_frame + workspace_area.x_size_frame) * 2) |
1216 | 0 | as f64 |
1217 | 0 | / (workspace_area.height + (workspace_area.y_fixed_frame + workspace_area.y_size_frame) * 2) |
1218 | 0 | as f64; |
1219 | | |
1220 | 0 | let grid_columns = max( |
1221 | 0 | ((number_of_consoles as f64).sqrt() * (aspect_ratio + aspect_ratio_adjustment)) as i32, |
1222 | | 1, |
1223 | | ); |
1224 | 0 | let grid_rows = max( |
1225 | 0 | (number_of_consoles as f64 / grid_columns as f64).ceil() as i32, |
1226 | | 1, |
1227 | | ); |
1228 | | |
1229 | 0 | let grid_column_index = index % grid_columns; |
1230 | 0 | let grid_row_index = index / grid_columns; |
1231 | | |
1232 | 0 | let is_last_row = grid_row_index == grid_rows - 1; |
1233 | 0 | let last_row_console_count = number_of_consoles % grid_columns; |
1234 | | |
1235 | 0 | let console_width = if is_last_row && last_row_console_count != 0 { |
1236 | 0 | (workspace_area.width / last_row_console_count) |
1237 | 0 | + if last_row_console_count > 1 { |
1238 | 0 | workspace_area.x_fixed_frame + workspace_area.x_size_frame |
1239 | | } else { |
1240 | 0 | 0 |
1241 | | } |
1242 | | } else { |
1243 | 0 | (workspace_area.width / grid_columns) |
1244 | 0 | + (workspace_area.x_fixed_frame + workspace_area.x_size_frame) |
1245 | | }; |
1246 | | |
1247 | 0 | let console_height = (workspace_area.height |
1248 | 0 | + (workspace_area.y_fixed_frame + workspace_area.y_size_frame) * grid_row_index) |
1249 | 0 | / grid_rows; |
1250 | | |
1251 | 0 | let x = grid_column_index * console_width |
1252 | 0 | - ((workspace_area.x_fixed_frame + workspace_area.x_size_frame) * (grid_column_index + 1)); |
1253 | 0 | let y = grid_row_index * console_height |
1254 | 0 | - ((workspace_area.y_fixed_frame + workspace_area.y_size_frame) * (grid_row_index - 1)); |
1255 | | |
1256 | 0 | return get_console_rect(x, y, console_width, console_height, workspace_area); |
1257 | 0 | } |
1258 | | |
1259 | | /// Transform the position and dimensions of a console window based |
1260 | | /// on the workspace area. |
1261 | | /// |
1262 | | /// To minimize empty space between windows, width and height must be adjusted |
1263 | | /// by the `fixed_frame` and `size_frame` values. |
1264 | | /// |
1265 | | /// # Arguments |
1266 | | /// |
1267 | | /// * `x` - The `x` coordinate of the window. |
1268 | | /// * `y` - The `y` coordinate of the window. |
1269 | | /// * `width` - The `width` in pixels of the window. |
1270 | | /// * `height` - The `height` in pixels of the window. |
1271 | | /// * `workspace_area` - The available workspace area on the primary monitor minus |
1272 | | /// the space occupied by the daemon console window. |
1273 | | /// |
1274 | | /// # Returns |
1275 | | /// |
1276 | | /// (`x`, `y`, `width`, `height`) |
1277 | | /// |
1278 | 0 | fn get_console_rect( |
1279 | 0 | x: i32, |
1280 | 0 | y: i32, |
1281 | 0 | width: i32, |
1282 | 0 | height: i32, |
1283 | 0 | workspace_area: &workspace::WorkspaceArea, |
1284 | 0 | ) -> (i32, i32, i32, i32) { |
1285 | 0 | return ( |
1286 | 0 | std::cmp::max( |
1287 | 0 | workspace_area.x - (workspace_area.x_fixed_frame + workspace_area.x_size_frame), |
1288 | 0 | workspace_area.x - (workspace_area.x_fixed_frame + workspace_area.x_size_frame) + x, |
1289 | 0 | ), |
1290 | 0 | workspace_area.y - (workspace_area.y_fixed_frame + workspace_area.y_size_frame) + y, |
1291 | 0 | std::cmp::min(workspace_area.width, width), |
1292 | 0 | height, |
1293 | 0 | ); |
1294 | 0 | } |
1295 | | |
1296 | | /// Spawns a background thread that ensures the z-order of all client |
1297 | | /// windows is in sync with the daemon window. |
1298 | | /// I.e. if the daemon window is focussed, all clients should be moved to the foreground. |
1299 | | /// |
1300 | | /// # Arguments |
1301 | | /// |
1302 | | /// * `windows_api` - Arc-wrapped Windows API implementation for thread-safe access |
1303 | | /// * `clients` - A thread safe mapping from the number |
1304 | | /// a client console window was launched at |
1305 | | /// in relation to the other client windows |
1306 | | /// and the clients console window handle. |
1307 | | /// The mapping must be thread safe to allow |
1308 | | /// it to be modified by the main thread |
1309 | | /// while we periodically read from it in the |
1310 | | /// background thread. |
1311 | 0 | fn ensure_client_z_order_in_sync_with_daemon<W: WindowsApi + Send + Sync + 'static>( |
1312 | 0 | windows_api: Arc<W>, |
1313 | 0 | clients: Arc<Mutex<Clients>>, |
1314 | 0 | ) { |
1315 | 0 | tokio::spawn(async move { |
1316 | 0 | let daemon_handle = get_console_window_wrapper(windows_api.as_ref()); |
1317 | 0 | let mut previous_foreground_window = get_foreground_window_wrapper(windows_api.as_ref()); |
1318 | | loop { |
1319 | 0 | tokio::time::sleep(Duration::from_millis(1)).await; |
1320 | 0 | let foreground_window = get_foreground_window_wrapper(windows_api.as_ref()); |
1321 | 0 | if previous_foreground_window == foreground_window { |
1322 | 0 | continue; |
1323 | 0 | } |
1324 | 0 | if foreground_window == daemon_handle |
1325 | 0 | && !clients.lock().unwrap().iter().any(|client| { |
1326 | 0 | return client.window_handle == previous_foreground_window.hwdn |
1327 | 0 | || client.window_handle == daemon_handle.hwdn; |
1328 | 0 | }) |
1329 | 0 | { |
1330 | 0 | defer_windows( |
1331 | 0 | windows_api.as_ref(), |
1332 | 0 | &clients.lock().unwrap(), |
1333 | 0 | &daemon_handle.hwdn, |
1334 | 0 | ); |
1335 | 0 | } |
1336 | 0 | previous_foreground_window = foreground_window; |
1337 | | } |
1338 | | }); |
1339 | 0 | } |
1340 | | |
1341 | | /// Move all given windows to the foreground. |
1342 | | /// |
1343 | | /// Restores minimized windows. |
1344 | | /// If a window handle no longer points to a valid window, it is skipped. |
1345 | | /// The daemon window is deferred last and receives focus. |
1346 | | /// |
1347 | | /// # Arguments |
1348 | | /// |
1349 | | /// * `windows_api` - The Windows API implementation to use |
1350 | | /// * `clients` - A thread safe mapping from the number |
1351 | | /// a client console window was launched at |
1352 | | /// in relation to the other client windows |
1353 | | /// and the clients console window handle. |
1354 | | /// * `daemon_handle` - Handle to the daemon console window. |
1355 | 0 | fn defer_windows<W: WindowsApi>(windows_api: &W, clients: &[Client], daemon_handle: &HWND) { |
1356 | 0 | for client in clients.iter().chain([&Client { |
1357 | 0 | hostname: "root".to_owned(), |
1358 | 0 | window_handle: *daemon_handle, |
1359 | 0 | process_handle: HANDLE::default(), |
1360 | 0 | process_id: 0, |
1361 | 0 | pipe_server_state: Arc::new(Mutex::new(PipeServerState::Enabled)), |
1362 | 0 | }]) { |
1363 | 0 | let placement = match windows_api.get_window_placement(client.window_handle) { |
1364 | 0 | Ok(placement) => placement, |
1365 | | Err(_) => { |
1366 | 0 | continue; |
1367 | | } |
1368 | | }; |
1369 | | // First restore if window is minimized |
1370 | 0 | if placement.showCmd == SW_SHOWMINIMIZED.0.try_into().unwrap() { |
1371 | 0 | let _ = windows_api.show_window(client.window_handle, SW_RESTORE); |
1372 | 0 | } |
1373 | | // Then bring it to front using UI automation |
1374 | 0 | let _ = windows_api.focus_window_with_automation(client.window_handle); |
1375 | | } |
1376 | 0 | } |
1377 | | |
1378 | | /// The entrypoint for the `daemon` subcommand. |
1379 | | /// |
1380 | | /// Spawns 1 client process with its own window for each host |
1381 | | /// and 1 worker thread that handles communication with the client |
1382 | | /// over a named pipe. |
1383 | | /// Responsible for client window positioning and sizing. |
1384 | | /// Handles control mode. |
1385 | | /// Main thread reads input records from the console input buffer |
1386 | | /// and propagates them via the background threads to all clients |
1387 | | /// simultaneously. |
1388 | | /// |
1389 | | /// # Arguments |
1390 | | /// |
1391 | | /// * `windows_api` - The Windows API implementation to use |
1392 | | /// * `hosts` - List of hostnames for which to launch clients. |
1393 | | /// * `username` - Username used to connect to the hosts. |
1394 | | /// If none, each client will use the SSH config to determine |
1395 | | /// a suitable username for their respective host. |
1396 | | /// * `port` - Optional port used for all SSH connections. |
1397 | | /// * `config` - The `DaemonConfig`. |
1398 | | /// * `debug` - Enables debug logging |
1399 | 0 | pub async fn main<W: WindowsApi + Clone + 'static>( |
1400 | 0 | windows_api: &W, |
1401 | 0 | hosts: Vec<String>, |
1402 | 0 | username: Option<String>, |
1403 | 0 | port: Option<u16>, |
1404 | 0 | config: &DaemonConfig, |
1405 | 0 | clusters: &[Cluster], |
1406 | 0 | debug: bool, |
1407 | 0 | ) { |
1408 | 0 | let daemon: Daemon = Daemon { |
1409 | 0 | hosts: explode(&hosts.join(" ")).unwrap_or(hosts), |
1410 | 0 | username, |
1411 | 0 | port, |
1412 | 0 | config, |
1413 | 0 | clusters, |
1414 | 0 | control_mode_state: ControlModeState::Inactive, |
1415 | 0 | debug, |
1416 | 0 | }; |
1417 | 0 | daemon.launch(windows_api).await; |
1418 | 0 | debug!("Actually exiting"); |
1419 | 0 | } |
1420 | | |
1421 | | #[cfg(test)] |
1422 | | #[path = "../tests/daemon/test_mod.rs"] |
1423 | | mod test_mod; |